// app/api/data-room/[projectId]/folders/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth/next'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { fileItems, fileActivityLogs } from '@/db/schema'; import { and, eq } from 'drizzle-orm'; import db from '@/db/db'; // 폴더 생성 export async function POST( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; // 세션 확인 const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); } // 내부 사용자만 폴더 생성 가능 if (session.user.domain === 'partners') { return NextResponse.json({ error: 'Permission denied' }, { status: 403 }); } // 요청 본문 파싱 const body = await request.json(); const { name, parentId, category } = body; // 필수 필드 검증 if (!name || typeof name !== 'string' || !name.trim()) { return NextResponse.json({ error: 'Folder name is required' }, { status: 400 }); } // 폴더 이름 정리 const folderName = name.trim(); // 폴더 이름 유효성 검사 const invalidChars = /[<>:"|?*]/; if (invalidChars.test(folderName)) { return NextResponse.json({ error: 'Folder name contains invalid characters' }, { status: 400 }); } // 부모 폴더 정보 조회 및 경로 설정 let parentFolder = null; let path = '/'; let depth = 0; let inheritedCategory = category || 'confidential'; // 기본값을 스키마에 맞게 변경 if (parentId && parentId !== null) { const parentFolderResult = await db .select() .from(fileItems) .where( and( eq(fileItems.id, parentId), eq(fileItems.projectId, projectId), eq(fileItems.type, 'folder') ) ) .limit(1); if (parentFolderResult.length === 0) { return NextResponse.json({ error: 'Parent folder not found' }, { status: 404 }); } parentFolder = parentFolderResult[0]; // 경로 계산 (부모 경로 + 부모 이름 + /) path = parentFolder.path + parentFolder.name + '/'; // depth는 부모 depth + 1 depth = (parentFolder.depth || 0) + 1; // 카테고리가 지정되지 않았으면 부모 폴더의 카테고리 상속 if (!category) { inheritedCategory = parentFolder.category || 'confidential'; } } // 같은 위치에 중복 이름 확인 const existingFolder = await db .select() .from(fileItems) .where( and( eq(fileItems.projectId, projectId), eq(fileItems.parentId, parentId || null), eq(fileItems.name, folderName), eq(fileItems.type, 'folder') ) ) .limit(1); if (existingFolder.length > 0) { return NextResponse.json({ error: 'A folder with this name already exists' }, { status: 409 }); } // 새 폴더 생성 (UUID는 자동 생성됨) const newFolder = { projectId, parentId: parentId || null, name: folderName, type: 'folder' as const, path, depth, category: inheritedCategory, size: 0, mimeType: 'folder', // filePath와 fileUrl은 폴더에서는 null filePath: null, fileUrl: null, // 권한 설정 externalAccessLevel: 'view_only' as const, externalAccessExpiry: null, downloadCount: 0, viewCount: 0, // 메타데이터 metadata: {}, tags: null, // 버전 관리 version: 1, previousVersionId: null, // 감사 로그 createdBy: Number(session.user.id), updatedBy: Number(session.user.id), createdAt: new Date(), updatedAt: new Date(), }; // DB에 폴더 저장 const [insertedFolder] = await db .insert(fileItems) .values(newFolder) .returning(); // 활동 로그 기록 (스키마가 있다면) if (fileActivityLogs) { try { await db.insert(fileActivityLogs).values({ fileItemId: insertedFolder.id, projectId, action: 'create_folder', actionDetails: { folderName: folderName, parentId: parentId || null, parentName: parentFolder?.name || null, path, category: inheritedCategory, depth, }, userId: Number(session.user.id), userEmail: session.user.email, userDomain: session.user.domain || 'default', ipAddress: request.ip || request.headers.get('x-forwarded-for') || null, userAgent: request.headers.get('user-agent') || null, createdAt: new Date(), }); } catch (logError) { // 로그 실패는 무시하고 계속 진행 console.error('Failed to create activity log:', logError); } } console.log('Folder created successfully:', { id: insertedFolder.id, name: folderName, path, depth, parentId: parentId || null, }); // 성공 응답 return NextResponse.json({ success: true, folder: insertedFolder, message: 'Folder created successfully', }, { status: 201 }); } catch (error) { console.error('Folder creation error:', error); // 에러 타입에 따른 응답 if (error instanceof Error) { // PostgreSQL unique constraint violation if (error.message.includes('unique') || error.message.includes('duplicate')) { return NextResponse.json( { error: 'A folder with this path already exists' }, { status: 409 } ); } // Foreign key constraint violation if (error.message.includes('foreign key')) { return NextResponse.json( { error: 'Invalid project or parent folder' }, { status: 400 } ); } // 데이터베이스 연결 오류 if (error.message.includes('connect')) { return NextResponse.json( { error: 'Database connection failed' }, { status: 503 } ); } } // 일반 오류 return NextResponse.json( { error: 'Failed to create folder', details: process.env.NODE_ENV === 'development' ? error?.message : undefined }, { status: 500 } ); } } // 폴더 목록 조회 export async function GET( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); } // URL 파라미터에서 parentId 가져오기 const { searchParams } = new URL(request.url); const parentId = searchParams.get('parentId'); // 폴더만 조회 const conditions = [ eq(fileItems.projectId, projectId), eq(fileItems.type, 'folder'), ]; if (parentId) { conditions.push(eq(fileItems.parentId, parentId)); } else { conditions.push(eq(fileItems.parentId, null)); } const folders = await db .select() .from(fileItems) .where(and(...conditions)) .orderBy(fileItems.name); // 파트너사 사용자의 경우 접근 가능한 폴더만 필터링 let filteredFolders = folders; if (session.user.domain === 'partners') { // category가 'confidential'이 아닌 폴더만 표시 filteredFolders = folders.filter(folder => folder.category !== 'confidential' || folder.externalAccessLevel !== null ); } return NextResponse.json({ folders: filteredFolders, count: filteredFolders.length, }); } catch (error) { console.error('Folder list error:', error); return NextResponse.json( { error: 'Failed to load folders' }, { status: 500 } ); } }